package cc.aliza.lib.mongo.core.decoder;

import cc.aliza.lib.mongo.core.AlizaEntity;
import cc.aliza.lib.mongo.core.InternalDao;
import cc.aliza.lib.mongo.core.annotations.Default;
import cc.aliza.lib.mongo.core.annotations.RefList;
import cc.aliza.lib.mongo.core.cache.ConstructorCache;
import cc.aliza.lib.mongo.core.cache.DaoCache;
import cc.aliza.lib.mongo.core.utils.DataType;
import cc.aliza.lib.mongo.core.utils.FieldUtil;
import cc.aliza.lib.mongo.core.utils.ReferenceUtil;
import com.mongodb.DBRef;
import org.bson.Document;

import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.WildcardType;
import java.util.*;

/**
 * 『给时光以生命·给岁月以文明』
 *
 * @author Zhang Jing (byrdkm17@gmail.com).
 */
@SuppressWarnings("unchecked")
public class RefListDecoder extends AbstractDecoder {

    private boolean cascadeR;

    public RefListDecoder(Document document, Field field) {
        super(field);
        RefList refList = field.getAnnotation(RefList.class);
        value = document.get(getFieldRealName(refList.name()));
        String cascade = refList.cascade().toUpperCase();
        cascadeR = cascade.contains(Default.CASCADE_READ);
    }

    @Override
    public void decode(Object obj) {
        Class<?> type = field.getType();
        if (type.isArray()) {
            decodeArray(obj, type.getComponentType());
        } else {
            ParameterizedType parameterizedType = (ParameterizedType) field.getGenericType();
            Type[] types = parameterizedType.getActualTypeArguments();
            int len = types.length;
            if (len == 1) {
                Class<?> clazz;
                if (types[0] instanceof WildcardType) {
                    clazz = (Class) ((WildcardType) types[0]).getUpperBounds()[0];
                } else {
                    clazz = (Class) types[0];
                }
                decodeCollection(obj, clazz, type);
            }
            if (len == 2) {
                decideMap(obj, (Class) types[1]);
            }
        }
    }

    private void decodeArray(Object obj, Class<?> clazz) {
        List<DBRef> list = (ArrayList) value;
        if (list.isEmpty()) {
            return;
        }
        List<AlizaEntity> result = new ArrayList<>();
        list.stream().filter(o -> o != null).forEach(o -> {
            Class<?> cls = FieldUtil.getRealType(field, clazz, o);
            AlizaEntity entity;
            if (cascadeR) {
                InternalDao dao = DaoCache.getInstance().get(cls);
                entity = (AlizaEntity) dao.findOne(ReferenceUtil.fromDBRef(o));
            } else {
                entity = (AlizaEntity) ConstructorCache.getInstance().get(cls);
                entity.setId(ReferenceUtil.fromDBRef(o));
            }
            result.add(entity);
        });
        FieldUtil.set(obj, field, result.toArray());
    }

    private void decodeCollection(Object obj, Class clazz, Class<?> type) {
        Collection<DBRef> collection = (Collection) value;
        if (collection.isEmpty()) {
            return;
        }
        List<AlizaEntity> result = new ArrayList<>();
        collection.stream().filter(o -> o != null).forEach(o -> {
            Class<?> cls = FieldUtil.getRealType(field, clazz, o);
            AlizaEntity entity;
            if (cascadeR) {
                InternalDao dao = DaoCache.getInstance().get(cls);
                entity = (AlizaEntity) dao.findOne(ReferenceUtil.fromDBRef(o));
            } else {
                entity = (AlizaEntity) ConstructorCache.getInstance().get(cls);
                entity.setId(ReferenceUtil.fromDBRef(o));
            }
            result.add(entity);
        });
        if (DataType.isListType(type)) {
            FieldUtil.set(obj, field, result);
        }
        if (DataType.isSetType(type)) {
            FieldUtil.set(obj, field, new HashSet(result));
        }
        if (DataType.isQueueType(type)) {
            FieldUtil.set(obj, field, new LinkedList(result));
        }
    }

    private void decideMap(Object obj, Class clazz) {
        Map<Object, DBRef> map = (Map) value;
        Map<Object, AlizaEntity> result = new HashMap<>();

        map.forEach((key, value) -> {
            if (value != null) {
                Class<?> cls = FieldUtil.getRealType(field, clazz, key);
                AlizaEntity entity;
                if (cascadeR) {
                    InternalDao dao = DaoCache.getInstance().get(clazz);
                    entity = (AlizaEntity) dao.findOne(ReferenceUtil.fromDBRef(value));
                } else {
                    entity = (AlizaEntity) ConstructorCache.getInstance().get(cls);
                    entity.setId(ReferenceUtil.fromDBRef(key));
                }
                result.put(key, entity);
            }
        });
        FieldUtil.set(obj, field, result);
    }
}
